﻿using System;
using System.Security.Cryptography;

namespace GJQT.ZPK
{
    /// <summary>
    /// Provides convenience methods to encrypt or decrypt a buffer of bytes
    /// using a block cipher that operates on 8-byte blocks. This class can 
    /// be used with CryptoStream to implement a crypto stream.
    /// </summary>
    internal abstract class BlockCipher : ICryptoTransform
    {
        private delegate void TransformBlockDelegate(ref uint v0, ref uint v1);
        private readonly TransformBlockDelegate transformBlock;

        public BlockCipher(CryptoStreamMode mode)
        {
            switch (mode)
            {
                case CryptoStreamMode.Read:
                    transformBlock = this.DecryptBlock;
                    break;
                case CryptoStreamMode.Write:
                    transformBlock = this.EncryptBlock;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("mode");
            }
        }

        protected virtual void EncryptBlock(ref uint v0, ref uint v1)
        {
            throw new NotImplementedException();
        }

        protected virtual void DecryptBlock(ref uint v0, ref uint v1)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
        }

        protected virtual void Dispose(bool disposing)
        {
        }

        public bool CanReuseTransform
        {
            get { return true; }
        }

        public bool CanTransformMultipleBlocks
        {
            get { return true; }
        }

        public int InputBlockSize
        {
            get { return 8; }
        }

        public int OutputBlockSize
        {
            get { return 8; }
        }

        public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
        {
            if (inputBuffer == null)
                throw new ArgumentNullException("inputBuffer");
            if (inputOffset < 0)
                throw new ArgumentOutOfRangeException("inputOffset");
            if (outputBuffer == null)
                throw new ArgumentNullException("outputBuffer");
            if (outputOffset < 0)
                throw new ArgumentOutOfRangeException("outputOffset");
            if (inputCount < 0 ||
                inputOffset + inputCount > inputBuffer.Length ||
                outputOffset + inputCount > outputBuffer.Length)
                throw new ArgumentOutOfRangeException("inputCount");
            if (inputCount % 8 != 0)
                throw new ArgumentException("inputCount must be a multiple of 8.");

            for (int count = inputCount / 8; count > 0; count--)
            {
                uint v0 = ReadBigEndian(inputBuffer, inputOffset);
                uint v1 = ReadBigEndian(inputBuffer, inputOffset + 4);

                transformBlock(ref v0, ref v1);

                WriteBigEndian(outputBuffer, outputOffset, v0);
                WriteBigEndian(outputBuffer, outputOffset + 4, v1);

                inputOffset += 8;
                outputOffset += 8;
            }
            return inputCount;
        }

        public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
        {
            if (inputBuffer == null)
                throw new ArgumentNullException("inputBuffer");
            if (inputOffset < 0)
                throw new ArgumentOutOfRangeException("inputOffset");
            if (inputCount < 0 || inputOffset + inputCount > inputBuffer.Length)
                throw new ArgumentOutOfRangeException("inputCount");
            if (inputCount % 8 != 0)
                throw new NotSupportedException("inputCount must be a multiple of 8.");

            byte[] outputBuffer = new byte[inputCount];
            TransformBlock(inputBuffer, inputOffset, inputCount, outputBuffer, 0);
            return outputBuffer;
        }

        private static uint ReadBigEndian(byte[] b, int i)
        {
            return (uint)((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]);
        }

        private static void WriteBigEndian(byte[] b, int i, uint v)
        {
            b[i + 0] = (byte)(v >> 24);
            b[i + 1] = (byte)(v >> 16);
            b[i + 2] = (byte)(v >> 8);
            b[i + 3] = (byte)(v >> 0);
        }
    }
}
